<?php

/*
Copyright 2009-2011 Sam Weiss
All Rights Reserved.

This file is part of Spark/Plug.

Spark/Plug is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

if (!defined('spark/plug'))
{
	header('HTTP/1.1 403 Forbidden');
	exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
}

//------------------------------------------------------------------------------

class SparkCRUDException extends SparkException
{
	public function __construct($message = NULL, $vars = NULL)
	{
		parent::__construct($message, 0, $vars);
	}
}

class SparkCRUDException_NotFound extends SparkCRUDException
{
	// resource not found
}

class SparkCRUDException_Conflict extends SparkCRUDException
{
	// resource conflicts with existing resource
}

class SparkCRUDException_Validation extends SparkCRUDException
{
	// resource data validation failure
}

class SparkCRUDException_Authorization extends SparkCRUDException
{
	// resource authorization failure
}

class SparkCRUDException_Quota extends SparkCRUDException
{
	// client quota exceeded
}

// -----------------------------------------------------------------------------

class SparkCRUDModel extends SparkDBModel
{
	private $_table;					// table name
	private $_columns;				// column info
	private $_validator;				// custom validator
	private $_modelValidator;		// model validator

	//---------------------------------------------------------------------------

	public function __construct($params, $table, $columns = NULL, $validator = NULL)
	{		
		parent::__construct($params);
		
		$this->_table = $table;
		$this->_columns = $columns;
		$this->_validator = $validator;
		$this->_modelValidator = NULL;
		
		if (empty($columns) && !empty($table))
		{
			$this->_columns = $this->loadModelDB(true)->getFunction('metadata')->columns($table);
		}
	}
	
	//---------------------------------------------------------------------------

	public function create(&$resource, $request)
	{
		$this->filterCreate($resource);
		$this->validateCreate($resource);

		$db = $this->loadModelDB(false);

		try
		{
			$db->insertRow($this->_table, $resource);
			$resource['id'] = $db->lastInsertID();
		}
		catch (SparkDBException $e)
		{
			switch ($e->dbErrorCode())
			{
				case SparkDBException::kDuplicateRecord:
					throw new SparkCRUDException_Conflict('resource exists');
			}
			throw $e;
		}
		catch (Exception $e)
		{
			throw $e;
		}
	}
	
	//---------------------------------------------------------------------------

	public function update($id, $resource, $request)
	{
		if (empty($id))
		{
			throw new SparkCRUDException_Validation('no resource id');
		}
		
		$this->filterUpdate($resource);
		$this->validateUpdate($resource);

		$this->getConstraints($where, $bind);
		$where[] = is_numeric($id) ? 'id=?' : 'uuid=?';
		$where = implode(' AND ', $where);
		$bind[] = $id;

		$db = $this->loadModelDB(false);

		try
		{
			if (!empty($resource))
			{
				$db->updateRows($this->_table, $resource, $where, $bind);
				if (!$db->affectedRows())
				{
					if (!$db->exists($this->_table, $where, $bind))
					{
						throw new SparkCRUDException_NotFound('resource not found');
					}
				}
			}
		}
		catch (SparkDBException $e)
		{
			switch ($e->dbErrorCode())
			{
				case SparkDBException::kDuplicateRecord:
					throw new SparkCRUDException_Conflict('resource exists');
			}
			throw $e;
		}
		catch (Exception $e)
		{
			throw $e;
		}
	}
	
	//---------------------------------------------------------------------------

	public function delete($resource)
	{
		$this->deleteByID(@$resource['id']);
	}
	
	//---------------------------------------------------------------------------

	public function deleteByID($id)
	{
		if (empty($id))
		{
			throw new SparkCRUDException_Validation('no resource id');
		}

		$this->getConstraints($where, $bind);
		$where[] = is_numeric($id) ? 'id=?' : 'uuid=?';
		$where = implode(' AND ', $where);
		$bind[] = $id;

		$db = $this->loadModelDB(false);
		
		try
		{
			$db->deleteRows($this->_table, $where, $bind);
			if (!$db->affectedRows())
			{
				throw new SparkCRUDException_NotFound('resource not found');
			}
		}
		catch (Exception $e)
		{
			throw $e;
		}
	}
	
	//---------------------------------------------------------------------------

	public function deleteAll()
	{
		$this->getConstraints($where, $bind);

		$this->loadModelDB(false)->deleteRows($this->_table, $where, $bind);
	}
	
	//---------------------------------------------------------------------------

	public function findByID($id, $columns = NULL)
	{
		if (empty($id))
		{
			throw new SparkCRUDException_Validation('no resource id');
		}

		$this->validateColumns($columns);
		
		$this->getConstraints($where, $bind);
		$where[] = is_numeric($id) ? 'id=?' : 'uuid=?';
		$where = implode(' AND ', $where);
		$bind[] = $id;

		$db = $this->loadModelDB(true);

		try
		{
			if (!$resource = $db->selectRow($this->_table, !empty($columns) ? $columns : '*', $where, $bind))
			{
				throw new SparkCRUDException_NotFound('resource not found');
			}
		}
		catch (SparkDBException $e)
		{
			switch ($e->dbErrorCode())
			{
				case SparkDBException::kUnknownColumn:
					throw new SparkCRUDException_Validation('unrecognized resource');
			}
			throw $e;
		}
		
		$this->filterShow($resource);
		return $resource;
	}
	
	//---------------------------------------------------------------------------

	public function findByFilter($filters = NULL, $columns = NULL)
	{
		$this->validateColumns($columns);

		$this->getConstraints($where, $bind);
		$where = implode(' AND ', $where);

		$db = $this->loadModelDB(true);

		$resources = $db->selectRows($this->_table, !empty($columns) ? $columns : '*', $where, $bind);
		
		foreach (array_keys($resources) as $key)
		{
			$this->filterShow($resources[$key]);
		}

		return $resources;
	}
	
	//---------------------------------------------------------------------------
	
	protected function getConstraints(&$where, &$bind)
	{
		$where = array();
		$bind = NULL;
	}

	//---------------------------------------------------------------------------
	
	protected function filterCreate(&$resource)
	{
		unset($resource['id']);
		$this->filter($resource);
	}

	//---------------------------------------------------------------------------
	
	protected function filterUpdate(&$resource)
	{
		unset($resource['id']);
		unset($resource['uuid']);
		$this->filter($resource);
	}

	//---------------------------------------------------------------------------
	
	protected function filterShow(&$resource)
	{
		$this->filter($resource);
	}

	//---------------------------------------------------------------------------
	
	protected function filter(&$resource)
	{
		if (!empty($resource))
		{
			$resource = array_intersect_key($resource, $this->_columns);
		}
	}

	//---------------------------------------------------------------------------
	
	protected function validateCreate(&$resource)
	{
		$this->validate($resource);
	}

	//---------------------------------------------------------------------------
	
	protected function validateUpdate(&$resource)
	{
		$this->validate($resource);
	}

	//---------------------------------------------------------------------------
	
	protected function validate(&$resource)
	{
		if (empty($resource))
		{
			throw new SparkCRUDException_Validation('empty request body');
		}

		if ($validator = $this->loadValidator())
		{
			if (!$validator->validate($resource, $errors))
			{
				throw new SparkCRUDException_Validation('invalid input', array('reason'=>'invalid input', 'errors'=>$errors));
			}
		}
	}

	//---------------------------------------------------------------------------

	protected function validateColumns(&$columns)
	{
		if (!empty($columns))
		{
			if (!is_array($columns))
			{
				$columns = array_map('trim', explode(',',  $columns));
			}
			
			foreach ($columns as $column)
			{
				if (!isset($this->_columns[$column]))
				{
					throw new SparkCRUDException_Validation('unrecognized resource');
				}
			}
			
			$columns = implode(',', $columns);
		}
	}
	
	//---------------------------------------------------------------------------
	
	protected function validateNotEmpty(&$resource, $columns)
	{
		foreach ($columns as $column)
		{
			if (empty($resource[$column]))
			{
				throw new SparkCRUDException_Validation('missing required field: '. $column);
			}
		}
	}

	//---------------------------------------------------------------------------

	protected function loadModelDB($readOnly = true)
	{
		return $this->loadDB();
	}
	
	//---------------------------------------------------------------------------

	protected function loadValidator()
	{
		if (!isset($this->_modelValidator))
		{
			$this->_modelValidator = $this->factory->manufacture('SparkValidator', $this->_columns, $this->_validator);
		}
		return $this->_modelValidator;
	}
	
	//---------------------------------------------------------------------------
	
}
